unit smGUI;

interface

uses
  Gen2, smExceptions, smUtils, smConst, smCommonTypes, smGUIRender, SysUtils,
  Classes, Data.DBXJSON;

type
  TGUI = class;
  TGUIElement = class;

  TGUIElementState = (gesNormal = 1, gesHovered = 2);
  TGUIElement = class
  protected
    var FOwner: TGUI;
    var FState: TGUIElementState;
    var FName: AnsiString;
    var FEnabled: Boolean;
    var FVisible: Boolean;
    var FSystemVisible: Boolean;
    var FPosition: TGameRect;
    var FBackground: Pointer;
    var FBackgroundColor: Cardinal;
    var FBorderColor: Cardinal;
    var FFont: Pointer;
    var FFontColor: Cardinal;
    var FDrag: Boolean;
    var FScene: Byte;

    procedure OnMouseMove(const X, Y: Integer); virtual;
    procedure OnMouseDown(const Button: Byte); virtual;
    procedure OnMouseUp(const Button: Byte); virtual;
    procedure OnKeyPress(const Key: AnsiChar); virtual;
    procedure OnStateChanging(const OldState: TGUIElementState); virtual;
    procedure OnSceneChanged(const NewScene: Byte); virtual;

    procedure SetState(const Value: TGUIElementState);
    procedure ParseJSON(const Source: TJSONPair); virtual;
    procedure AppendCommand(const ACmd: AnsiString);

    function GetVisible(): Boolean;

  public
    property Owner: TGUI read FOwner;
    property State: TGUIElementState read FState write SetState;
    property Name: AnsiString read FName write FName;
    property Enabled: Boolean read FEnabled write FEnabled;
    property Visible: Boolean read GetVisible write FVisible;
    property Position: TGameRect read FPosition;
    property Left: Integer read FPosition.Left;
    property Top: Integer read FPosition.Top;
    property Width: Integer read FPosition.Width;
    property Height: Integer read FPosition.Height;
    property Background: Pointer read FBackground;
    property BackgroundColor: Cardinal read FBackgroundColor
      write FBackgroundColor;
    property BorderColor: Cardinal read FBorderColor write FBorderColor;
    property Font: Pointer read FFont;
    property FontColor: Cardinal read FFontColor write FFontColor;
    property Drag: Boolean read FDrag;
    property Scene: Byte read FScene write FScene;

    function LoadFromJSON(const Source: TJSONObject): Boolean;
    function MouseOver(const MX, MY: Integer): Boolean; virtual; abstract;
    procedure Perform(const Message: TGUIMessage; const Lo, Hi: Integer;
      var Handled: Boolean); virtual;

    constructor Create(const AOwner: Pointer); reintroduce; virtual;
  end;

  TGUIState = (gsCreated, gsLoading, gsReady, gsModal);
  TGUIStates = set of TGUIState;
  TGUI = class
  protected
    // Grouping modal related variables
    type TGUIModality = record
      Color: Cardinal;
      Panel: TGUIElement;
      Result: Integer;
      StoredPos: TGamePoint;
      StoredVisible: Boolean;
      StoredEnabled: Boolean;
    end;

    var FStates: TGUIStates;
    var FClientRect: TGameRect;
    var FModality: TGUIModality;
    var FBackgroundColor: Cardinal;
    var FBackground: Pointer; // texture
    var FFont: Pointer;
    var FPanels: TGameList;
    var FRenderer: TGUIRender;
    var FCommandPool: array of AnsiString;
    var FTextures: TExTextureMgr;

    function GetElement(const Name: AnsiString): TGUIElement;
    function LoadFromJSON(const Source: TJSONObject): Boolean;

  public
    property States: TGUIStates read FStates;
    property ClientRect: TGameRect read FClientRect;
    property ModalColor: Cardinal read FModality.Color;
    property ModalPanel: TGUIElement read FModality.Panel;
    property ModalResult: Integer read FModality.Result;
    property BackgroundColor: Cardinal read FBackgroundColor;
    property Background: Pointer read FBackground;
    property Font: Pointer read FFont;
    property Panels[const Name: AnsiString]: TGUIElement
      read GetElement; default;
    property PanelList: TGameList read FPanels;
    property Textures: TExTextureMgr read FTextures;

    function Initialize(const Nested: Boolean = False): Boolean; overload;
    function Initialize(const FileName: String): Boolean; overload;
    procedure AddPanel();
    procedure ShowModal(const Panel: TGUIElement); overload;
    procedure ShowModal(const PanelName: AnsiString); overload;
    procedure ReleaseModal(const Result: Integer);
    procedure AppendCommand(const ACommand: AnsiString);

    function Dispatch(const Message: TGUIMessage;
      const Lo, Hi: Integer): Boolean; reintroduce;
    procedure Render();
    procedure Clear();

    constructor Create();
    destructor Destroy(); override;
  end;

var
  GUIManager: TGUI;

implementation

uses
  smMain, smGUIPanel, smLuaUtils, smClassGUI;

{ TGUI }

constructor TGUI.Create;
begin
  inherited Create();
  FStates := [];

  FPanels := TGameList.Create();
  FPanels.ObjectElements := True;

  FRenderer := TGUIRender.Create();
  Engine.RequestMod(TExTextureMgr, @FTextures);
end;

function TGUI.GetElement(const Name: AnsiString): TGUIElement;
var
  I: Integer;
begin
  for I := 0 to FPanels.Count - 1 do
{$R-}
    if TGUIElement(FPanels.Raw[I]).Name = Name then
    begin
      Result := FPanels.Raw[I];
      Exit;
    end;
{$R+}
  Result := nil;
end;

function TGUI.LoadFromJSON(const Source: TJSONObject): Boolean;

  function LoadClientRect(const Source: TJSONObject): Boolean;
  var
    I: Integer;
  begin
    for I := 0 to Source.Size - 1 do
      with Source.Get(I) do
        if JsonString.Value = 'left' then
          Self.FClientRect.Left := TJSONNumber(JsonValue).AsInt

        else if JsonString.Value = 'top' then
          Self.FClientRect.Top := TJSONNumber(JsonValue).AsInt

        else if JsonString.Value = 'width' then
          Self.FClientRect.Width := TJSONNumber(JsonValue).AsInt

        else if JsonString.Value = 'height' then
          Self.FClientRect.Height := TJSONNumber(JsonValue).AsInt

        else if JsonString.Value = 'right' then
          Self.FClientRect.SetRight(TJSONNumber(JsonValue).AsInt)

        else if JsonString.Value = 'bottom' then
          Self.FClientRect.SetBottom(TJSONNumber(JsonValue).AsInt);

    Result := True;
  end;

  function LoadPanel(const Source: TJSONObject): Boolean;
  var
    TempPanel: TGUIPanel;
  begin
    TempPanel := TGUIPanel.Create(Self);
    Result := TempPanel.LoadFromJSON(Source as TJSONObject);

    if not Result then
      FPanels.RemoveRelease(TempPanel)
  end;

var
  I: Integer;
begin
  try
    for I := 0 to Source.Size - 1 do
      with Source.Get(I) do
        if JsonString.Value = 'background' then
          Self.FBackground := FTextures.NeedTexture(
              AnsiString(JsonValue.Value)
            )

        else if JsonString.Value = 'background_color' then
          Self.FBackgroundColor := Cardinal(TJSONNumber(JsonValue).AsInt)

        else if JsonString.Value = 'modal_color' then
          Self.FModality.Color := Cardinal(TJSONNumber(JsonValue).AsInt)

        else if JsonString.Value = 'client_rect' then
        begin
          if not LoadClientRect(JsonValue as TJSONObject) then
            raise EGUILoadingParamFailed.Create(
              JsonString.Value, JsonValue.Value
            )
        end

        else if JsonString.Value = 'panel' then
        begin
          if not LoadPanel(JsonValue as TJSONObject) then
            raise EGUILoadingParamFailed.Create(
              JsonString.Value, JsonValue.Value
            )
        end;

    Result := True;
  except
    Result := False;
  end;
end;

function TGUI.Initialize(const Nested: Boolean): Boolean;
begin
  Clear();

  // Basic initialization
  FStates := [gsCreated];

  with FClientRect do
  begin
    Left := 0;
    Top  := 0;
    Width  := ScreenWidth;
    Height := ScreenHeight;
  end;
  FModality.Color  := DefaultGUIModalColor;
  FModality.Panel  := nil;
  FModality.Result := 0;
  FBackgroundColor := DefaultGUIBackgroundColor;
  FBackground      := nil;
  FFont            := Game.FontSystem;

  if Nested then
    FStates := FStates + [gsLoading]
  else
    FStates := [gsReady];
  Result := True;
end;

function TGUI.Initialize(const FileName: String): Boolean;
var
  Source: TJSONObject;
begin
  Result := Initialize(True);
  Source := nil;

  if Result then
    try
      // Loading textures
      FTextures.OpenPack(Format('data\%s.txp', [FileName]));

      // continue initialization...
      with TStringStream.Create() do
        try
          // todo: Load from PE-resource
          LoadFromFile(Format('core\%s.gui', [FileName]));
          Source := TJSONObject.ParseJSONValue(
              Bytes, 0, Size, True
            ) as TJSONObject;

          if not (Assigned(Source) and LoadFromJSON(Source)) then
            raise EGUILoadingGeneral.Create('error loading GUI file');

        finally
          if Assigned(Source) then
            Source.Free();
          Free();
        end;

      FStates := [gsCreated, gsReady];
    except
      Result := False;
    end;
end;

procedure TGUI.AddPanel;
begin
  with TGUIPanel.Create(Self) do
  begin
    Position.SetValue(
      FPanels.Count * 10, FPanels.Count * 10,
      DefaultGUIElementWidth,
      DefaultGUIElementHeight
    );

    AddButton();
  end;
end;

procedure TGUI.ShowModal(const Panel: TGUIElement);
begin
  // Actual procedure
  if not (gsModal in States) then
    if Assigned(Panel) then
    begin
      FModality.Panel         := Panel;
      FModality.StoredEnabled := Panel.Enabled; // store current state
      FModality.StoredVisible := Panel.Visible;
      FModality.StoredPos     := Panel.Position;
      // Set new position in the center of playable area
      Panel.Position.SetValue(
        ClientRect.Width div 2 - Panel.Position.Width div 2,
        ClientRect.Height div 2 - Panel.Position.Height div 2,
        Panel.Position.Width, Panel.Position.Height
      );
      Panel.Enabled := True;
      Panel.Visible := True;

      FStates := FStates + [gsModal];
    end
    else
      EGUIModalFailed.Create('panel does not exist');
end;

procedure TGUI.ShowModal(const PanelName: AnsiString);
begin
  ShowModal(Panels[PanelName]);
end;

procedure TGUI.ReleaseModal;
begin
  if gsModal in States then
    if Assigned(FModality.Panel) then
    begin
      FModality.Result := Result;
      ModalPanel.Enabled := FModality.StoredEnabled;
      ModalPanel.Visible := FModality.StoredVisible;
      ModalPanel.Position.SetValue(
        FModality.StoredPos.X, FModality.StoredPos.Y,
        ModalPanel.Position.Width,
        ModalPanel.Position.Height
      );

      FStates := FStates - [gsModal];

      // Update Lua global
      SetFieldInt(luaCLASS_GUI, 'ModalResult', ModalResult);
    end
    else
      EGUIModalFailed.Create('no modal panel set');
end;

function TGUI.Dispatch;
var
  I: Integer;
begin
  Result := False;
  if gsModal in States then
    ModalPanel.Perform(Message, Lo, Hi, Result)
  else
    for I := FPanels.Count - 1 downto 0 do
{$R-}
      TGUIElement(FPanels[I]).Perform(Message, Lo, Hi, Result);
{$R+}

  // When all messages are done check the command pool
  // and trig all the stored objects there
  for I := 0 to High(FCommandPool) do
    RunScript(FCommandPool[I]);
  Finalize(FCommandPool);

end;

procedure TGUI.Render;
var
  I: Integer;
  ModalState: Boolean;
begin
  if gsReady in States then
  begin
    FRenderer.DrawGUI(Self);

    ModalState := gsModal in States;

    for I := 0 to FPanels.Count - 1 do
{$R-}
      if not ModalState or (FPanels[I] <> ModalPanel) then
        if TGUIElement(FPanels[I]).Visible then
          FRenderer.DrawPanel(FPanels[I]);
{$R+}

    if ModalState then
      FRenderer.DrawPanelModal(ModalPanel);
  end;
end;

procedure TGUI.AppendCommand(const ACommand: AnsiString);
begin
  SetLength(FCommandPool, Length(FCommandPool) + 1);
  FCommandPool[High(FCommandPool)] := ACommand;
end;

procedure TGUI.Clear;
begin
  FPanels.Clear();
  FTextures.FreeTextures();
end;

destructor TGUI.Destroy;
begin
  Clear();
  Engine.ReleaseMod(@FTextures);
  FRenderer.Free();
  FPanels.Free();
  inherited;
end;

{ TGUIElement }

procedure TGUIElement.AppendCommand(const ACmd: AnsiString);
begin
  Owner.AppendCommand(ACmd);
end;

constructor TGUIElement.Create;
begin
  inherited Create();
  if Assigned(AOwner) then
  begin
    FOwner := AOwner;
    FFont  := Owner.Font;
  end;

  FState  := gesNormal;
  Enabled := True;
  Visible := True;

  Position.SetValue(0, 0, DefaultGUIElementWidth, DefaultGUIElementHeight);

  FBackground := nil;
  FBackgroundColor := DefaultGUIElementBackgroundColor;
  FBorderColor     := DefaultGUIElementBorderColor;
  FontColor := DefaultGUIFontColor;
  FDrag     := False;
  FScene    := Byte(-1);
  FSystemVisible := True;

end;

function TGUIElement.GetVisible: Boolean;
begin
  Result := FVisible and FSystemVisible;
end;

procedure TGUIElement.OnMouseDown(const Button: Byte);
begin
  // do nothing...
end;

procedure TGUIElement.OnMouseMove(const X, Y: Integer);
begin
  // do nothing...
end;

procedure TGUIElement.OnMouseUp(const Button: Byte);
begin
  // do nothing...
end;

procedure TGUIElement.OnSceneChanged(const NewScene: Byte);
begin
  // do nothing...
end;

procedure TGUIElement.OnStateChanging(const OldState: TGUIElementState);
begin
  // do nothing...
end;

procedure TGUIElement.OnKeyPress(const Key: AnsiChar);
begin
  // do nothing...
end;

procedure TGUIElement.SetState(const Value: TGUIElementState);
begin
  if State <> Value then
  begin
    OnStateChanging(Value);
    FState := Value;
  end;
end;

procedure TGUIElement.ParseJSON(const Source: TJSONPair);

  function LoadFont(const Source: TJSONObject): Boolean;
  var
    I: Integer;
    TmpName: String;
    TmpSize: Integer;
  begin
    TmpName := 'arial';
    TmpSize := 10;

    for I := 0 to Source.Size - 1 do
      with Source.Get(I) do
        if JsonString.Value = 'family' then
          TmpName := JsonValue.Value

        else if JsonString.Value = 'size' then
          TmpSize := BoundI(TJSONNumber(JsonValue).AsInt, 10, 28)

        else if JsonString.Value = 'color' then
          FFontColor := Cardinal(TJSONNumber(JsonValue).AsInt);

    Self.FFont := Game.Gfx.Shared.RequestFont(AnsiString(TmpName), TmpSize);
    Result := True;
  end;

begin
  with Source, Source.JsonString do
    if Value = 'name' then
      Name := AnsiString(JsonValue.Value)

    else if Value = 'enabled' then
      Enabled := TJSONBoolean(JsonValue).AsBoolean

    else if Value = 'visible' then
      Visible := TJSONBoolean(JsonValue).AsBoolean

    else if Value = 'left' then
      FPosition.Left := TJSONNumber(JsonValue).AsInt

    else if Value = 'top' then
      FPosition.Top := TJSONNumber(JsonValue).AsInt

    else if Value = 'width' then
      FPosition.Width := TJSONNumber(JsonValue).AsInt

    else if Value = 'height' then
      FPosition.Height := TJSONNumber(JsonValue).AsInt

    else if Value = 'right' then
      FPosition.SetRight(
        Owner.ClientRect.Width - TJSONNumber(JsonValue).AsInt
      )

    else if Value = 'bottom' then
      FPosition.SetBottom(
        Owner.ClientRect.Height - TJSONNumber(JsonValue).AsInt
      )

    else if Value = 'background' then
      FBackground := Owner.FTextures.NeedTexture(AnsiString(JsonValue.Value))

    else if Value = 'background_color' then
      FBackgroundColor := Cardinal(TJSONNumber(JsonValue).AsInt)

    else if Value = 'border_color' then
      FBorderColor := Cardinal(TJSONNumber(JsonValue).AsInt)

    else if Value = 'font' then
    begin
      if not LoadFont(JsonValue as TJSONObject) then
        raise EGUILoadingParamFailed.Create(
          Value, JsonValue.Value
        );
    end

    else if Value = 'font_color' then
      FFontColor := Cardinal(TJSONNumber(JsonValue).AsInt)

    else if Value = 'scene' then
      Scene := Byte(TJSONNumber(JsonValue).AsInt);
end;

function TGUIElement.LoadFromJSON(const Source: TJSONObject): Boolean;
var
  I: Integer;
begin
  try
    for I := 0 to Source.Size - 1 do
      ParseJSON(Source.Get(I));

    Result := True;
  except
    Result := False;
  end;
end;

procedure TGUIElement.Perform(const Message: TGUIMessage; const Lo, Hi: Integer;
  var Handled: Boolean);
var
  NewMouseOver: Boolean;
begin
  case Message of
    // Mouse move event
    gmMouseMove:
      begin
        NewMouseOver := MouseOver(Lo, Hi);
        if Handled then
          State := gesNormal
        else
        begin
          Handled := NewMouseOver;
          if NewMouseOver or FDrag then
          begin
            State := gesHovered;
            OnMouseMove(Lo, Hi);
          end
          else
            State := gesNormal;
        end;

      end;

    // Mouse down
    gmMouseDown:
      if not Handled and (State = gesHovered) then
      begin
        Handled := True;
        if Lo = 0 then
          FDrag := True;
        OnMouseDown(Lo);
      end;

    // Mouse up
    gmMouseUp:
      begin
        if not Handled and (State = gesHovered) then
        begin
          Handled := True;
          OnMouseUp(Lo);
        end;
        if Lo = 0 then
          FDrag := False;
      end;

    // Key down
    gmKeyPress:
      if (Lo = 27) and (gsModal in Owner.States) then // key Esc
        Owner.ReleaseModal(ModalResult_CANCEL)
      else
        if Enabled and Visible and not Handled then
        begin
          Handled := True;
          OnKeyPress(AnsiChar(Lo));
        end;

    // On scene change
    gmSceneChanged:
      begin
        FSystemVisible := Byte(Lo) and Scene = Byte(Lo);
        if Enabled then
          OnSceneChanged(Byte(Lo));
      end;

  end;

end;

end.
